using System;
using System.Collections.Generic;
using System.Text;
using System.Diagnostics;

namespace PortfolioAllocator.Optimization
{
    /// <summary>Mapping between date and multi security values (prices, returns)</summary>
    struct DateMultiValues
    {
        private List<DateTime> datesM;  // row dates
        private double[,] valuesM;  // dates in rows(ix=0), and securities in columns (ix=1)
        private int nOfRowsM;       // number of rows (dates)

        public DateMultiValues(List<DateTime> dates, double[,] values)
        {
            if (dates.Count != values.GetLength(0))
                throw new Exception("DateMultiValues date size missmatch: dates.Count=" + dates.Count + ", values.rows=" + values.GetLength(1));
            this.datesM = dates;
            this.valuesM = values;
            this.nOfRowsM = dates.Count;
        }
        public List<DateTime> Dates
        {
            get { return this.datesM; }
        }
        public double[,] Values
        {
            get { return this.valuesM; }
        }
        public int NOfRows
        {
            get { return this.nOfRowsM; }
        }

        public override string ToString()
        {
            StringBuilder sb = new StringBuilder();
            int nOfSec = Values.GetLength(1);
            for (int dIx = 0; dIx < Dates.Count; ++dIx)
            {
                DateTime date = Dates[dIx];
                sb.Append(date.ToShortDateString() + ":");
                for (int sIx = 0; sIx < nOfSec; ++sIx)
                    sb.Append("\t" + this.Values[dIx, sIx]);
                sb.AppendLine();
            }
            return sb.ToString();
        }
    }

    class MultiValueUtilities
    {
        /// <summary>find intersecting dates forseveral time series and create dated table with dates in row and securities in columns</summary>
        /// <param name="datedPrices">List of several dated time series</param>
        /// <returns>Array of prices on intersecting dates with dates in rows (ix=0) and securities in column (ix=1)</returns>
        static public DateMultiValues findIntersection(List<SortedList<DateTime, double>> datedValues)
        {
            int nOfSecurities = datedValues.Count;
            if (nOfSecurities == 0)
                return new DateMultiValues(new List<DateTime>(0), new double[0, 0]);

            // Find all intersectin dates
            IList<DateTime> dates0 = datedValues[0].Keys;  // all dates for the first time series
            List<DateTime> commonDates = new List<DateTime>(dates0.Count);
            foreach (DateTime d in dates0)
            {
                bool isInAllTS = true;
                for (int sIx = 0; sIx < nOfSecurities; ++sIx)
                    if (datedValues[sIx].IndexOfKey(d) == -1)
                    {
                        isInAllTS = false;
                        break;
                    }
                if (isInAllTS)
                    commonDates.Add(d);
            }
            // Find values on intersecting dates
            int nOfDates = commonDates.Count;
            double[,] values = new double[nOfDates, nOfSecurities];
            for (int dIx = 0; dIx < nOfDates; ++dIx)
            {
                DateTime d = commonDates[dIx];
                for (int sIx = 0; sIx < nOfSecurities; ++sIx)
                    values[dIx, sIx] = datedValues[sIx][d];
            }

            return new DateMultiValues(commonDates, values);
        }

        /// <summary>Given table with dated prices calculate dated table with log returns</summary>
        /// <param name="prices">Dated table with security prices</param>
        /// <returns>Dated table with log returns. The same number of rows as prices. The first row = 0</returns>
        static public DateMultiValues calcLogReturns(DateMultiValues prices)
        {
            int nOfDates = prices.Dates.Count;
            int nOfSec = prices.Values.GetLength(1);
            double[,] logRets = new double[nOfDates, nOfSec];
            for (int dIx = 1; dIx < nOfDates; ++dIx)
                for (int sIx = 0; sIx < nOfSec; ++sIx)
                {
                    logRets[dIx, sIx] = Math.Log(prices.Values[dIx, sIx] / prices.Values[dIx - 1, sIx]);
                }
            return new DateMultiValues(prices.Dates, logRets);
        }

        #region Exponentially Weighted Covariance Calculations

        /// <summary>Recursive formula for exponentially weighted covariance calculations</summary>
        /// <param name="covAB0">covariance between A and B at time t (yesterday)</param>
        /// <param name="nExp">Period for exponential weighting.  2/(1+n) = 1-lambda in RiskMetrics notation</param>
        /// <param name="rA1">Log return of security A at time t+1 (today). rA1 = ln(priceA(t+1)/priceA(t))</param>
        /// <param name="rB1">Log return of security B at time t+1 (today). rB1 = ln(priceB(t+1)/priceB(t))</param>
        /// <returns>covariance between A and B at time t+1 (today)</returns>
        public static double calcExpCovarAB(double covAB0, int nExp, double rA1, double rB1)
        {
            // covAB(t+1) = covAB(t) + 2/(1+n) * (rA(t+1)*rB(t+1) - covAB(t))
            return covAB0 + 2 * (rA1 * rB1 - covAB0) / (1 + nExp);
        }

        /// <summary>Calculate Exp weighted covariance matrix</summary>
        /// <param name="logRets">n x m array with log returns: n=number of dates (in rows), m=number of securities in columns </param>
        /// <param name="covStart">m x m array: Initial covariance matrix at index startIx</param>
        /// <param name="startIx">The first (row) index to start the calculations from. Covariance matrix at rowIx=startIx equals to covStartIx</param>
        /// <param name="endIx">(exclusive) The last (row) index to calculate covariance matrix. The result is the covariance index at rowIx=endIx-1</param>
        /// <param name="nExp">Period for exponential weighting.  2/(1+n) = 1-lambda in RiskMetrics notation</param>
        /// <returns>m x m array: Covariance matrix at endIx</returns>
        public static double[,] calcExpCovarAB(double[,] logRets, double[,] covStart, int startIx, int endIx, int nExp)
        {
            int n = logRets.GetLength(0);   // number of rows = number of dates
            int m = logRets.GetLength(1);   // number of cols = number of securities
            double[,] covMatrix = new double[m, m];
            Debug.Assert(covStart.GetLength(0) == m, "Number of rows in covariance matrix must equal to number of securities in log-return array", "m=" + m + ", covStart.dim=" + covStart.GetLength(0) + " x " + covStart.GetLength(1));
            Debug.Assert(covStart.GetLength(1) == m, "Number of cols in covariance matrix must equal to number of securities in log-return array", "m=" + m + ", covStart.dim=" + covStart.GetLength(0) + " x " + covStart.GetLength(1));
            Debug.Assert(startIx >= 0 && startIx < n, "StartIx is out of bounds", "n=" + n + ", startIx = " + startIx);
            Debug.Assert(endIx >= 0 && endIx <= n, "endIx is out of bounds", "n=" + n + ", endIx = " + endIx);
            Debug.Assert(endIx > startIx, "endIx must be larger than startIx", "startIx=" + startIx + ", endIx=" + endIx);

            for (int rowIx = 0; rowIx < m; ++rowIx)
                for (int colIx = rowIx; colIx < m; ++colIx)
                {
                    double covAB = covStart[rowIx, colIx];
                    for (int dateIx = startIx + 1; dateIx < endIx; ++dateIx)
                        covAB = calcExpCovarAB(covAB, nExp, logRets[dateIx, colIx], logRets[dateIx, rowIx]);
                    covMatrix[rowIx, colIx] = covAB;
                    covMatrix[colIx, rowIx] = covAB;
                }
            return covMatrix;
        }

        /// <summary>Annualize covariance matrix based on daily returns</summary>
        /// <param name="covDaily">Covariance matrix calculated from daily returns</param>
        /// <returns>Covariance matrix relevan to annual returns</returns>
        public static double[,] annualizeCovarianceMatrix(double[,] covDaily)
        {
            int n = covDaily.GetLength(0);
            Debug.Assert(n == covDaily.GetLength(1), "Covariance matrix must be quare");
            const int nOfDaysInYear = 252;

            double[,] cov = new double[n,n];
            for (int i = 0; i < n; ++i)
                for (int j = 0; j <= i; ++j)
                {
                    cov[i, j] = covDaily[i, j] * nOfDaysInYear;
                    cov[j, i] = covDaily[j, i] * nOfDaysInYear;
                }
            return cov;
        }

        /// <summary>Create a diagonal matrix of dimension "dim" with "typical" colatility. Used for to initiate covariance matrix</summary>
        /// <param name="n">Matrix dimension</param>
        /// <returns>Covariance matrix estimate</returns>
        public static double[,] createWarmUpCovarianceMatrix(int n)
        {
            double[,] cov = new double[n, n];
            double vol = 0.25 / Math.Sqrt(252);
            for (int i = 0; i < n; ++i)
                cov[i, i] = vol * vol;
            return cov;
        }

        #endregion 
    }
}